/*
  +----------------------------------------------------------------------+
  | Copyright (c) The PHP Group                                          |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | https://www.php.net/license/3_01.txt                                 |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Authors: Edin Kadribasic <edink@emini.dk>                            |
  |          Ilia Alshanestsky <ilia@prohost.org>                        |
  |          Wez Furlong <wez@php.net>                                   |
  +----------------------------------------------------------------------+
*/

#include "cat_pq.h"
#ifdef CAT_PQ

#if !defined(__cplusplus) && !defined(_MSC_VER)
#pragma GCC diagnostic push
# ifdef HAVE_WTYPEDEF_REDEFINITION
#  pragma GCC diagnostic ignored "-Wtypedef-redefinition"
# endif
#endif

// from ext/pdo_pgsql/pgsql_driver.c @ f7ca8138e7fcb90f7b42509d0489be166258b519

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "ext/standard/php_string.h"
#include "main/php_network.h"
#include "ext/pdo/php_pdo.h"
#include "ext/pdo/php_pdo_driver.h"
#include "ext/pdo/php_pdo_error.h"
#include "ext/standard/file.h"
#undef SIZEOF_OFF_T
#include "swow_pdo_pgsql_int.h"
#include "zend_exceptions.h"
#include "zend_smart_str.h"

#if !defined(__cplusplus) && !defined(_MSC_VER)
#pragma GCC diagnostic pop
#endif

#undef ZEND_METHOD
#define ZEND_METHOD(classname, name) ZEND_NAMED_FUNCTION(swow_zim_##classname##_##name)
#undef ZEND_ME
#if PHP_VERSION_ID < 80400
#define ZEND_ME(classname, name, arg_info, flags) ZEND_RAW_FENTRY(#name, swow_zim_##classname##_##name, arg_info, flags)
#else
#define ZEND_ME(classname, name, arg_info, flags) ZEND_RAW_FENTRY(#name, swow_zim_##classname##_##name, arg_info, flags, NULL, NULL)
#endif
#if PHP_VERSION_ID < 80100
// diff since php/php-src@efbb2198d4e5a01167d1a8c2937b848f75ac4d19
#undef ZEND_ATOL
// from Zend/zend_long.h
#ifdef ZEND_ENABLE_ZVAL_LONG64
# ifdef ZEND_WIN32
#  define ZEND_ATOL(s) _atoi64((s))
# else
#  define ZEND_ATOL(s) atoll((s))
# endif
#else
# ifdef ZEND_WIN32
#  define ZEND_ATOL(s) atol((s))
# else
#  define ZEND_ATOL(s) atol((s))
# endif
#endif
#endif // PHP_VERSION_ID
// diff since php/php-src@6728c1bd72f2ddb46528a0c61ac833b1036a12a2
#if PHP_VERSION_ID < 80100
#define swow_pdo_txn_bool int
#else
#define swow_pdo_txn_bool bool
#endif

#include "swow_pgsql_driver_arginfo.h"

static swow_pdo_txn_bool pgsql_handle_in_transaction(pdo_dbh_t *dbh);

static char * _pdo_pgsql_trim_message(const char *message, int persistent)
{
    size_t i = strlen(message);
    char *tmp;
    if (UNEXPECTED(i == 0)) {
        tmp = pemalloc(1, persistent);
        tmp[0] = '\0';
        return tmp;
    }
    --i;

    if (i>1 && (message[i-1] == '\r' || message[i-1] == '\n') && message[i] == '.') {
        --i;
    }
    while (i>0 && (message[i] == '\r' || message[i] == '\n')) {
        --i;
    }
    ++i;
    tmp = pemalloc(i + 1, persistent);
    memcpy(tmp, message, i);
    tmp[i] = '\0';

    return tmp;
}

static zend_string* _pdo_pgsql_escape_credentials(char *str)
{
    if (str) {
        return php_addcslashes_str(str, strlen(str), "\\'", sizeof("\\'"));
    }

    return NULL;
}

int _swow_pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const char *sqlstate, const char *msg, const char *file, int line) /* {{{ */
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_error_type *pdo_err = stmt ? &stmt->error_code : &dbh->error_code;
    pdo_pgsql_error_info *einfo = &H->einfo;
    char *errmsg = PQerrorMessage(H->server);

    einfo->errcode = errcode;
    einfo->file = file;
    einfo->line = line;

    if (einfo->errmsg) {
        pefree(einfo->errmsg, dbh->is_persistent);
        einfo->errmsg = NULL;
    }

    if (sqlstate == NULL || strlen(sqlstate) >= sizeof(pdo_error_type)) {
        strcpy(*pdo_err, "HY000");
    }
    else {
        strcpy(*pdo_err, sqlstate);
    }

    if (msg) {
        einfo->errmsg = pestrdup(msg, dbh->is_persistent);
    }
    else if (errmsg) {
        einfo->errmsg = _pdo_pgsql_trim_message(errmsg, dbh->is_persistent);
    }

    if (!dbh->methods) {
        pdo_throw_exception(einfo->errcode, einfo->errmsg, pdo_err);
    }

    return errcode;
}
/* }}} */


// diff since php/php-src@a9259c04969eefabf4c66a8843a66d0bee1c56c0
#if PHP_VERSION_ID < 80400
static void _pdo_pgsql_notice(pdo_dbh_t *dbh, const char *message) /* {{{ */
{
/*    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; */
}
/* }}} */
#else
static void _pdo_pgsql_notice(void *context, const char *message) /* {{{ */
{
    pdo_dbh_t * dbh = (pdo_dbh_t *)context;
    swow_fcall_info_cache *fc = ((pdo_pgsql_db_handle *)dbh->driver_data)->notice_callback;
    if (fc) {
        zval zarg;
        ZVAL_STRING(&zarg, message);
        swow_call_known_fcc(fc, NULL, 1, &zarg, NULL);
        zval_ptr_dtor_str(&zarg);
    }
}
/* }}} */
#endif // PHP_VERSION_ID

// diff since php/php-src@53ba72ec03b05800de94c3e4f3f8c96aae70185d
#if PHP_VERSION_ID < 80100
static int pdo_pgsql_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *info) /* {{{ */
#else
static void pdo_pgsql_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *info) /* {{{ */
#endif // PHP_VERSION_ID
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_pgsql_error_info *einfo = &H->einfo;

    if (einfo->errcode) {
        add_next_index_long(info, einfo->errcode);
    } else {
        /* Add null to respect expected info array structure */
        add_next_index_null(info);
    }
    if (einfo->errmsg) {
        add_next_index_string(info, einfo->errmsg);
    }
#if PHP_VERSION_ID < 80100
    return 1;
#endif // PHP_VERSION_ID
}
/* }}} */

static void swow_pdo_pgsql_cleanup_notice_callback(pdo_pgsql_db_handle *H) /* {{{ */
{
    if (H->notice_callback) {
        swow_fcc_dtor(H->notice_callback);
        efree(H->notice_callback);
        H->notice_callback = NULL;
    }
}
/* }}} */

/* {{{ pdo_pgsql_create_lob_stream */
static ssize_t swow_pgsql_lob_write(php_stream *stream, const char *buf, size_t count)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    return lo_write(self->conn, self->lfd, (char*)buf, count);
}

static ssize_t swow_pgsql_lob_read(php_stream *stream, char *buf, size_t count)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    return lo_read(self->conn, self->lfd, buf, count);
}

static int swow_pgsql_lob_close(php_stream *stream, int close_handle)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(Z_PDO_DBH_P(&self->dbh))->driver_data;

    if (close_handle) {
        lo_close(self->conn, self->lfd);
    }
    zend_hash_index_del(H->lob_streams, php_stream_get_resource_id(stream));
    zval_ptr_dtor(&self->dbh);
    efree(self);
    return 0;
}

static int swow_pgsql_lob_flush(php_stream *stream)
{
    return 0;
}

static int swow_pgsql_lob_seek(php_stream *stream, zend_off_t offset, int whence,
        zend_off_t *newoffset)
{
    struct pdo_pgsql_lob_self *self = (struct pdo_pgsql_lob_self*)stream->abstract;
#ifdef ZEND_ENABLE_ZVAL_LONG64
    zend_off_t pos = lo_lseek64(self->conn, self->lfd, offset, whence);
#else
    zend_off_t pos = lo_lseek(self->conn, self->lfd, offset, whence);
#endif
    *newoffset = pos;
    return pos >= 0 ? 0 : -1;
}

const php_stream_ops swow_pdo_pgsql_lob_stream_ops = {
    swow_pgsql_lob_write,
    swow_pgsql_lob_read,
    swow_pgsql_lob_close,
    swow_pgsql_lob_flush,
    "pdo_pgsql lob stream",
    swow_pgsql_lob_seek,
    NULL,
    NULL,
    NULL
};

php_stream *swow_pdo_pgsql_create_lob_stream(zend_object *dbh, int lfd, Oid oid)
{
    php_stream *stm;
    struct pdo_pgsql_lob_self *self = ecalloc(1, sizeof(*self));
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)(php_pdo_dbh_fetch_inner(dbh))->driver_data;

    ZVAL_OBJ(&self->dbh, dbh);
    self->lfd = lfd;
    self->oid = oid;
    self->conn = H->server;

    stm = php_stream_alloc(&swow_pdo_pgsql_lob_stream_ops, self, 0, "r+b");

    if (stm) {
            GC_ADDREF(dbh);
            zend_hash_index_add_ptr(H->lob_streams, php_stream_get_resource_id(stm), stm->res);
            return stm;
    }

    efree(self);
    return NULL;
}
/* }}} */

void swow_pdo_pgsql_close_lob_streams(pdo_dbh_t *dbh)
{
    zend_resource *res;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (H->lob_streams) {
        ZEND_HASH_REVERSE_FOREACH_PTR(H->lob_streams, res) {
            if (res->type >= 0) {
                zend_list_close(res);
            }
        } ZEND_HASH_FOREACH_END();
    }
}

// diff since php/php-src@53ba72ec03b05800de94c3e4f3f8c96aae70185d
#if PHP_VERSION_ID < 80100
static int pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
#else
static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
#endif // PHP_VERSION_ID
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (H) {
        if (H->lob_streams) {
            swow_pdo_pgsql_close_lob_streams(dbh);
            zend_hash_destroy(H->lob_streams);
            pefree(H->lob_streams, dbh->is_persistent);
            H->lob_streams = NULL;
        }
        swow_pdo_pgsql_cleanup_notice_callback(H);
        if (H->server) {
            PQfinish(H->server);
            H->server = NULL;
        }
        if (H->einfo.errmsg) {
            pefree(H->einfo.errmsg, dbh->is_persistent);
            H->einfo.errmsg = NULL;
        }
        pefree(H, dbh->is_persistent);
        dbh->driver_data = NULL;
    }
// diff since php/php-src@53ba72ec03b05800de94c3e4f3f8c96aae70185d
#if PHP_VERSION_ID < 80100
    return 0;
#endif // PHP_VERSION_ID
}
/* }}} */

// diff since php/php-src@2d51c203f09551323ed595514e03ab206fd93129
#if PHP_VERSION_ID < 80100
static int pgsql_handle_preparer(pdo_dbh_t *dbh, const char *sql, size_t sql_len, pdo_stmt_t *stmt, zval *driver_options)
#else
static bool pgsql_handle_preparer(pdo_dbh_t *dbh, zend_string *sql, pdo_stmt_t *stmt, zval *driver_options)
#endif // PHP_VERSION_ID
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    pdo_pgsql_stmt *S = ecalloc(1, sizeof(pdo_pgsql_stmt));
    int scrollable;
    int ret;
// diff since php/php-src@2d51c203f09551323ed595514e03ab206fd93129
#if PHP_VERSION_ID < 80100
    char *nsql = NULL;
    size_t nsql_len = 0;
#else
    zend_string *nsql = NULL;
#endif // PHP_VERSION_ID
    int emulate = 0;
    int execute_only = 0;
    zval *val;
    zend_long lval;

    S->H = H;
    stmt->driver_data = S;
    stmt->methods = &swow_pgsql_stmt_methods;

    scrollable = pdo_attr_lval(driver_options, PDO_ATTR_CURSOR,
        PDO_CURSOR_FWDONLY) == PDO_CURSOR_SCROLL;

    if (scrollable) {
        if (S->cursor_name) {
            efree(S->cursor_name);
        }
        spprintf(&S->cursor_name, 0, "pdo_crsr_%08x", ++H->stmt_counter);
        emulate = 1;
    } else if (driver_options) {
        if (pdo_attr_lval(driver_options, PDO_ATTR_EMULATE_PREPARES, H->emulate_prepares) == 1) {
            emulate = 1;
        }
        if (pdo_attr_lval(driver_options, PDO_PGSQL_ATTR_DISABLE_PREPARES, H->disable_prepares) == 1) {
            execute_only = 1;
        }
    } else {
        emulate = H->emulate_prepares;
        execute_only = H->disable_prepares;
    }

    /* PQprotocolVersion 2 removed in php/php-src@95f829db13e0ebb5a36844292f6947700fef8811 */

    if (emulate) {
        stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
    } else {
        stmt->supports_placeholders = PDO_PLACEHOLDER_NAMED;
        stmt->named_rewrite_template = "$%d";
    }

    S->is_unbuffered =
        driver_options
        && (val = zend_hash_index_find(Z_ARRVAL_P(driver_options), PDO_ATTR_PREFETCH))
        && pdo_get_long_param(&lval, val)
        ? !lval
        : H->default_fetching_laziness
    ;

// diff since php/php-src@2d51c203f09551323ed595514e03ab206fd93129
#if PHP_VERSION_ID < 80100
    ret = pdo_parse_params(stmt, (char*)sql, sql_len, &nsql, &nsql_len);
#else
    ret = pdo_parse_params(stmt, sql, &nsql);
#endif // PHP_VERSION_ID

    if (ret == -1) {
        /* couldn't grok it */
        strcpy(dbh->error_code, stmt->error_code);
        return false;
    } else if (ret == 1) {
        /* query was re-written */
        S->query = nsql;
    } else {
// diff since php/php-src@2d51c203f09551323ed595514e03ab206fd93129
#if PHP_VERSION_ID < 80100
        S->query = estrdup(sql);
#else
        S->query = zend_string_copy(sql);
#endif // PHP_VERSION_ID
    }

    if (!emulate && !execute_only) {
        /* prepared query: set the query name and defer the
           actual prepare until the first execute call */
        spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
    }

    return true;
}

// diff since php/php-src@94ea8e247bb8103e346666dd33de08905b3e67ab
#if PHP_VERSION_ID < 80100
static zend_long pgsql_handle_doer(pdo_dbh_t *dbh, const char *sql, size_t sql_len)
#else
static zend_long pgsql_handle_doer(pdo_dbh_t *dbh, const zend_string *sql)
#endif // PHP_VERSION_ID
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    PGresult *res;
    zend_long ret = 1;
    ExecStatusType qs;

    bool in_trans = pgsql_handle_in_transaction(dbh);

#if PHP_VERSION_ID < 80100
    if (!(res = cat_pq_exec(H->server, sql))) {
#else
    if (!(res = cat_pq_exec(H->server, ZSTR_VAL(sql)))) {
#endif // PHP_VERSION_ID

        /* fatal error */
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
        return -1;
    }
    qs = PQresultStatus(res);
    if (qs != PGRES_COMMAND_OK && qs != PGRES_TUPLES_OK) {
        pdo_pgsql_error(dbh, qs, pdo_pgsql_sqlstate(res));
        PQclear(res);
        return -1;
    }
    H->pgoid = PQoidValue(res);
    if (qs == PGRES_COMMAND_OK) {
        ret = ZEND_ATOL(PQcmdTuples(res));
    } else {
        ret = Z_L(0);
    }
    PQclear(res);
    if (in_trans && !pgsql_handle_in_transaction(dbh)) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    }

    return ret;
}

// diff since php/php-src@63cda0fea83d19d17d19df18d712328372e7891c
#if PHP_VERSION_ID < 80100
static int pgsql_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t unquotedlen, char **quoted, size_t *quotedlen, enum pdo_param_type paramtype)
{
    unsigned char *escaped;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    size_t tmp_len;

    switch (paramtype) {
        case PDO_PARAM_LOB:
            /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */
            escaped = PQescapeByteaConn(H->server, (unsigned char *)unquoted, unquotedlen, &tmp_len);
            *quotedlen = tmp_len + 1;
            *quoted = emalloc(*quotedlen + 1);
            memcpy((*quoted)+1, escaped, *quotedlen-2);
            (*quoted)[0] = '\'';
            (*quoted)[*quotedlen-1] = '\'';
            (*quoted)[*quotedlen] = '\0';
            PQfreemem(escaped);
            break;
        default:
            *quoted = safe_emalloc(2, unquotedlen, 3);
            (*quoted)[0] = '\'';
            *quotedlen = PQescapeStringConn(H->server, *quoted + 1, unquoted, unquotedlen, NULL);
            (*quoted)[*quotedlen + 1] = '\'';
            (*quoted)[*quotedlen + 2] = '\0';
            *quotedlen += 2;
    }
    return 1;
}
#else
static zend_string* pgsql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquoted, enum pdo_param_type paramtype)
{
    unsigned char *escaped;
    char *quoted;
    size_t quotedlen;
    zend_string *quoted_str;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    size_t tmp_len;
    int err;

    switch (paramtype) {
        case PDO_PARAM_LOB:
            /* escapedlen returned by PQescapeBytea() accounts for trailing 0 */
            escaped = PQescapeByteaConn(H->server, (unsigned char *)ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &tmp_len);
            if (escaped == NULL) {
                return NULL;
            }
            quotedlen = tmp_len + 1;
            quoted = emalloc(quotedlen + 1);
            memcpy(quoted+1, escaped, quotedlen-2);
            quoted[0] = '\'';
            quoted[quotedlen-1] = '\'';
            quoted[quotedlen] = '\0';
            PQfreemem(escaped);
            break;
        default:
            quoted = safe_emalloc(2, ZSTR_LEN(unquoted), 3);
            quoted[0] = '\'';
            quotedlen = PQescapeStringConn(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), &err);
            if (err) {
                efree(quoted);
                return NULL;
            }
            quoted[quotedlen + 1] = '\'';
            quoted[quotedlen + 2] = '\0';
            quotedlen += 2;
    }

    quoted_str = zend_string_init(quoted, quotedlen, 0);
    efree(quoted);
    return quoted_str;
}
#endif // PHP_VERSION_ID

// diff since php/php-src@a78aea8948255718952471db9f96d882aa28dc7d
#if PHP_VERSION_ID < 80100
static char *pdo_pgsql_last_insert_id(pdo_dbh_t *dbh, const char *name, size_t *len)
#else
static zend_string *pdo_pgsql_last_insert_id(pdo_dbh_t *dbh, const zend_string *name)
#endif // PHP_VERSION_ID
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
#if PHP_VERSION_ID < 80100
    char *id = NULL;
#else
    zend_string *id = NULL;
#endif // PHP_VERSION_ID
    PGresult *res;
    ExecStatusType status;

    if (name == NULL) {
        res = cat_pq_exec(H->server, "SELECT LASTVAL()");
    } else {
        const char *q[1];
#if PHP_VERSION_ID < 80100
        q[0] = name;
#else
        q[0] = ZSTR_VAL(name);
#endif // PHP_VERSION_ID

        res = cat_pq_exec_params(H->server, "SELECT CURRVAL($1)", 1, NULL, q, NULL, NULL, 0);
    }
    status = PQresultStatus(res);

    if (res && (status == PGRES_TUPLES_OK)) {
#if PHP_VERSION_ID < 80100
        id = estrdup((char *)PQgetvalue(res, 0, 0));
        *len = PQgetlength(res, 0, 0);
#else
        id = zend_string_init((char *)PQgetvalue(res, 0, 0), PQgetlength(res, 0, 0), 0);
#endif // PHP_VERSION_ID
    } else {
        pdo_pgsql_error(dbh, status, pdo_pgsql_sqlstate(res));
    }

    if (res) {
        PQclear(res);
    }

    return id;
}

void swow_pdo_libpq_version(char *buf, size_t len)
{
    int version = PQlibVersion();
    int major = version / 10000;
    if (major >= 10) {
        int minor = version % 10000;
        snprintf(buf, len, "%d.%d", major, minor);
    } else {
        int minor = version / 100 % 100;
        int revision = version % 100;
        snprintf(buf, len, "%d.%d.%d", major, minor, revision);
    }
}

static int pdo_pgsql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_value)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    switch (attr) {
        case PDO_ATTR_EMULATE_PREPARES:
            ZVAL_BOOL(return_value, H->emulate_prepares);
            break;

        case PDO_PGSQL_ATTR_DISABLE_PREPARES:
            ZVAL_BOOL(return_value, H->disable_prepares);
            break;

        case PDO_ATTR_CLIENT_VERSION: {
            char buf[16];
            swow_pdo_libpq_version(buf, sizeof(buf));
            ZVAL_STRING(return_value, buf);
            break;
        }

        case PDO_ATTR_SERVER_VERSION:
            /* PQprotocolVersion 2 removed in php/php-src@95f829db13e0ebb5a36844292f6947700fef8811 */
            ZVAL_STRING(return_value, (char*)PQparameterStatus(H->server, "server_version"));
            break;

        case PDO_ATTR_CONNECTION_STATUS:
            switch (PQstatus(H->server)) {
                case CONNECTION_STARTED:
                    ZVAL_STRINGL(return_value, "Waiting for connection to be made.", strlen("Waiting for connection to be made."));
                    break;

                case CONNECTION_MADE:
                case CONNECTION_OK:
                    ZVAL_STRINGL(return_value, "Connection OK; waiting to send.", strlen("Connection OK; waiting to send."));
                    break;

                case CONNECTION_AWAITING_RESPONSE:
                    ZVAL_STRINGL(return_value, "Waiting for a response from the server.", strlen("Waiting for a response from the server."));
                    break;

                case CONNECTION_AUTH_OK:
                    ZVAL_STRINGL(return_value, "Received authentication; waiting for backend start-up to finish.", strlen("Received authentication; waiting for backend start-up to finish."));
                    break;
#ifdef CONNECTION_SSL_STARTUP
                case CONNECTION_SSL_STARTUP:
                    ZVAL_STRINGL(return_value, "Negotiating SSL encryption.", strlen("Negotiating SSL encryption."));
                    break;
#endif
                case CONNECTION_SETENV:
                    ZVAL_STRINGL(return_value, "Negotiating environment-driven parameter settings.", strlen("Negotiating environment-driven parameter settings."));
                    break;

#ifdef CONNECTION_CONSUME
                case CONNECTION_CONSUME:
                    ZVAL_STRINGL(return_value, "Flushing send queue/consuming extra data.", strlen("Flushing send queue/consuming extra data."));
                    break;
#endif
#ifdef CONNECTION_GSS_STARTUP
                case CONNECTION_SSL_STARTUP:
                    ZVAL_STRINGL(return_value, "Negotiating GSSAPI.", strlen("Negotiating GSSAPI."));
                    break;
#endif
#ifdef CONNECTION_CHECK_TARGET
                case CONNECTION_CHECK_TARGET:
                    ZVAL_STRINGL(return_value, "Connection OK; checking target server properties.", strlen("Connection OK; checking target server properties."));
                    break;
#endif
#ifdef CONNECTION_CHECK_STANDBY
                case CONNECTION_CHECK_STANDBY:
                    ZVAL_STRINGL(return_value, "Connection OK; checking if server in standby.", strlen("Connection OK; checking if server in standby."));
                    break;
#endif
                case CONNECTION_BAD:
                default:
                    ZVAL_STRINGL(return_value, "Bad connection.", strlen("Bad connection."));
                    break;
            }
            break;

        case PDO_ATTR_SERVER_INFO: {
            int spid = PQbackendPID(H->server);


            zend_string *str_info =
                strpprintf(0,
                    "PID: %d; Client Encoding: %s; Is Superuser: %s; Session Authorization: %s; Date Style: %s",
                    spid,
                    (char*)PQparameterStatus(H->server, "client_encoding"),
                    (char*)PQparameterStatus(H->server, "is_superuser"),
                    (char*)PQparameterStatus(H->server, "session_authorization"),
                    (char*)PQparameterStatus(H->server, "DateStyle"));

            ZVAL_STR(return_value, str_info);
            break;
        }

        default:
            return 0;
    }

    return 1;
}

/* {{{ */
// diff since php/php-src@6728c1bd72f2ddb46528a0c61ac833b1036a12a2
#if PHP_VERSION_ID < 80100
static int pdo_pgsql_check_liveness(pdo_dbh_t *dbh)
#else
static zend_result pdo_pgsql_check_liveness(pdo_dbh_t *dbh)
#endif // PHP_VERSION_ID
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    if (!PQconsumeInput(H->server) || PQstatus(H->server) == CONNECTION_BAD) {
        PQreset(H->server);
    }
    return (PQstatus(H->server) == CONNECTION_OK) ? SUCCESS : FAILURE;
}
/* }}} */

static swow_pdo_txn_bool pgsql_handle_in_transaction(pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    return PQtransactionStatus(H->server) > PQTRANS_IDLE;
}

static swow_pdo_txn_bool pdo_pgsql_transaction_cmd(const char *cmd, pdo_dbh_t *dbh)
{
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
    PGresult *res;
    bool ret = true;

    res = cat_pq_exec(H->server, cmd);

    if (PQresultStatus(res) != PGRES_COMMAND_OK) {
        pdo_pgsql_error(dbh, PQresultStatus(res), pdo_pgsql_sqlstate(res));
        ret = false;
    }

    PQclear(res);
    return ret;
}

static swow_pdo_txn_bool pgsql_handle_begin(pdo_dbh_t *dbh)
{
    return pdo_pgsql_transaction_cmd("BEGIN", dbh);
}

static swow_pdo_txn_bool pgsql_handle_commit(pdo_dbh_t *dbh)
{
    swow_pdo_txn_bool ret = pdo_pgsql_transaction_cmd("COMMIT", dbh);

    /* When deferred constraints are used the commit could
       fail, and a ROLLBACK implicitly ran. See bug #67462 */
    if (ret) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    } else {
        dbh->in_txn = pgsql_handle_in_transaction(dbh);
    }

    return ret;
}

static swow_pdo_txn_bool pgsql_handle_rollback(pdo_dbh_t *dbh)
{
    int ret = pdo_pgsql_transaction_cmd("ROLLBACK", dbh);

    if (ret) {
        swow_pdo_pgsql_close_lob_streams(dbh);
    }

    return ret;
}

static bool _pdo_pgsql_send_copy_data(pdo_pgsql_db_handle *H, zval *line) {
    size_t query_len;
    zend_string *query;

    if (!try_convert_to_string(line)) {
        return false;
    }

    query_len = Z_STRLEN_P(line);
    query = zend_string_alloc(query_len + 2, false); /* room for \n\0 */
    memcpy(ZSTR_VAL(query), Z_STRVAL_P(line), query_len + 1);
    ZSTR_LEN(query) = query_len;

    if (query_len > 0 && ZSTR_VAL(query)[query_len - 1] != '\n') {
        ZSTR_VAL(query)[query_len] = '\n';
        ZSTR_VAL(query)[query_len + 1] = '\0';
        ZSTR_LEN(query) ++;
    }

    if (PQputCopyData(H->server, ZSTR_VAL(query), ZSTR_LEN(query)) != 1) {
        zend_string_release_ex(query, false);
        return false;
    }

    zend_string_release_ex(query, false);
    return true;
}

void swow_pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    zval *pg_rows;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sA|sss!",
        &table_name, &table_name_len, &pg_rows,
        &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    if ((Z_TYPE_P(pg_rows) != IS_ARRAY && !instanceof_function(Z_OBJCE_P(pg_rows), zend_ce_traversable))) {
        zend_argument_type_error(2, "must be of type array or Traversable");
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }

    /* Obtain db Handle */
    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }
    pgsql_result = cat_pq_exec(H->server, query);

    efree(query);
    query = NULL;

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_IN && pgsql_result) {
        int command_failed = 0;
        zval *tmp;
        zend_object_iterator *iter;

        PQclear(pgsql_result);

        if (Z_TYPE_P(pg_rows) == IS_ARRAY) {
            ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pg_rows), tmp) {
                if (!_pdo_pgsql_send_copy_data(H, tmp)) {
                    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                    PDO_HANDLE_DBH_ERR();
                    RETURN_FALSE;
                }
            } ZEND_HASH_FOREACH_END();
        } else {
            iter = Z_OBJ_P(pg_rows)->ce->get_iterator(Z_OBJCE_P(pg_rows), pg_rows, 0);
            if (iter == NULL || EG(exception)) {
                RETURN_THROWS();
            }

            for (; iter->funcs->valid(iter) == SUCCESS && EG(exception) == NULL; iter->funcs->move_forward(iter)) {
                tmp = iter->funcs->get_current_data(iter);
                if (!_pdo_pgsql_send_copy_data(H, tmp)) {
                    zend_iterator_dtor(iter);
                    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                    PDO_HANDLE_DBH_ERR();
                    RETURN_FALSE;
                }
            }
            zend_iterator_dtor(iter);
        }

        if (PQputCopyEnd(H->server, NULL) != 1) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            if (PGRES_COMMAND_OK != PQresultStatus(pgsql_result)) {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
                command_failed = 1;
            }
            PQclear(pgsql_result);
        }

        PDO_HANDLE_DBH_ERR();
        RETURN_BOOL(!command_failed);
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyFromArray)
{
    swow_pgsqlCopyFromArray_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

void swow_pgsqlCopyFromFile_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *filename, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t  table_name_len, filename_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;
    PGresult *pgsql_result;
    ExecStatusType status;
    php_stream *stream;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sp|sss!",
        &table_name, &table_name_len, &filename, &filename_len,
        &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    /* Obtain db Handler */
    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    stream = php_stream_open_wrapper_ex(filename, "rb", 0, NULL, FG(default_context));
    if (!stream) {
        pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to open the file");
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s FROM STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }
    pgsql_result = cat_pq_exec(H->server, query);

    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_IN && pgsql_result) {
        char *buf;
        int command_failed = 0;
        size_t line_len = 0;

        PQclear(pgsql_result);
        while ((buf = php_stream_get_line(stream, NULL, 0, &line_len)) != NULL) {
            if (PQputCopyData(H->server, buf, line_len) != 1) {
                efree(buf);
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                php_stream_close(stream);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
            efree(buf);
        }
        php_stream_close(stream);

        if (PQputCopyEnd(H->server, NULL) != 1) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            if (PGRES_COMMAND_OK != PQresultStatus(pgsql_result)) {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
                command_failed = 1;
            }
            PQclear(pgsql_result);
        }

        PDO_HANDLE_DBH_ERR();
        RETURN_BOOL(!command_failed);
    } else {
        php_stream_close(stream);
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyFromFile)
{
    swow_pgsqlCopyFromFile_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

void swow_pgsqlCopyToFile_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL, *filename = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len, filename_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    php_stream *stream;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "sp|sss!",
                    &table_name, &table_name_len, &filename, &filename_len,
                    &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    stream = php_stream_open_wrapper_ex(filename, "wb", 0, NULL, FG(default_context));
    if (!stream) {
        pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to open the file for writing");
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }
    pgsql_result = cat_pq_exec(H->server, query);
    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_OUT && pgsql_result) {
        PQclear(pgsql_result);
        while (1) {
            char *csv = NULL;
            int ret = PQgetCopyData(H->server, &csv, 0);

            if (ret == -1) {
                break; /* done */
            } else if (ret > 0) {
                if (php_stream_write(stream, csv, ret) != (ssize_t)ret) {
                    pdo_pgsql_error_msg(dbh, PGRES_FATAL_ERROR, "Unable to write to file");
                    PQfreemem(csv);
                    php_stream_close(stream);
                    PDO_HANDLE_DBH_ERR();
                    RETURN_FALSE;
                } else {
                    PQfreemem(csv);
                }
            } else {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                php_stream_close(stream);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        }
        php_stream_close(stream);

        while ((pgsql_result = PQgetResult(H->server))) {
            PQclear(pgsql_result);
        }
        RETURN_TRUE;
    } else {
        php_stream_close(stream);
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyToFile)
{
    swow_pgsqlCopyToFile_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);

}
/* }}} */

void swow_pgsqlCopyToArray_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    char *table_name, *pg_delim = NULL, *pg_null_as = NULL, *pg_fields = NULL;
    size_t table_name_len, pg_delim_len = 0, pg_null_as_len = 0, pg_fields_len;
    char *query;

    PGresult *pgsql_result;
    ExecStatusType status;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|sss!",
        &table_name, &table_name_len,
        &pg_delim, &pg_delim_len, &pg_null_as, &pg_null_as_len, &pg_fields, &pg_fields_len) == FAILURE) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    while ((pgsql_result = PQgetResult(H->server))) {
        PQclear(pgsql_result);
    }

    /* using pre-9.0 syntax as PDO_pgsql is 7.4+ compatible */
    if (pg_fields) {
        spprintf(&query, 0, "COPY %s (%s) TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, pg_fields, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    } else {
        spprintf(&query, 0, "COPY %s TO STDIN WITH DELIMITER E'%c' NULL AS E'%s'", table_name, (pg_delim_len ? *pg_delim : '\t'), (pg_null_as_len ? pg_null_as : "\\\\N"));
    }
    pgsql_result = cat_pq_exec(H->server, query);
    efree(query);

    if (pgsql_result) {
        status = PQresultStatus(pgsql_result);
    } else {
        status = (ExecStatusType) PQstatus(H->server);
    }

    if (status == PGRES_COPY_OUT && pgsql_result) {
        PQclear(pgsql_result);
                array_init(return_value);

        while (1) {
            char *csv = NULL;
            int ret = PQgetCopyData(H->server, &csv, 0);
            if (ret == -1) {
                break; /* copy done */
            } else if (ret > 0) {
                add_next_index_stringl(return_value, csv, ret);
                PQfreemem(csv);
            } else {
                pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
                PDO_HANDLE_DBH_ERR();
                RETURN_FALSE;
            }
        }

        while ((pgsql_result = PQgetResult(H->server))) {
            PQclear(pgsql_result);
        }
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, pdo_pgsql_sqlstate(pgsql_result));
        PQclear(pgsql_result);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
}

/* {{{ Returns true if the copy worked fine or false if error */
PHP_METHOD(PDO_PGSql_Ext, pgsqlCopyToArray)
{
    swow_pgsqlCopyToArray_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

void swow_pgsqlLOBCreate_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid lfd;

    ZEND_PARSE_PARAMETERS_NONE();

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;
    lfd = lo_creat(H->server, INV_READ|INV_WRITE);

    if (lfd != InvalidOid) {
        zend_string *buf = strpprintf(0, ZEND_ULONG_FMT, (zend_long) lfd);

        RETURN_STR(buf);
    }

    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}

/* {{{ Creates a new large object, returning its identifier.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBCreate)
{
    swow_pgsqlLOBCreate_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

void swow_pgsqlLOBOpen_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid oid;
    int lfd;
    char *oidstr;
    size_t oidstrlen;
    char *modestr = (char *) "rb";
    size_t modestrlen;
    int mode = INV_READ;
    char *end_ptr;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "s|s",
                &oidstr, &oidstrlen, &modestr, &modestrlen)) {
        RETURN_THROWS();
    }

    oid = (Oid)strtoul(oidstr, &end_ptr, 10);
    if (oid == 0 && (errno == ERANGE || errno == EINVAL)) {
        RETURN_FALSE;
    }

    if (strpbrk(modestr, "+w")) {
        mode = INV_READ|INV_WRITE;
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    lfd = lo_open(H->server, oid, mode);

    if (lfd >= 0) {
        php_stream *stream = swow_pdo_pgsql_create_lob_stream(Z_OBJ_P(ZEND_THIS), lfd, oid);
        if (stream) {
            php_stream_to_zval(stream, return_value);
            return;
        }
    } else {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    }

    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}

/* {{{ Opens an existing large object stream.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBOpen)
{
    swow_pgsqlLOBOpen_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

void swow_pgsqlLOBUnlink_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    Oid oid;
    char *oidstr, *end_ptr;
    size_t oidlen;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "s",
                &oidstr, &oidlen)) {
        RETURN_THROWS();
    }

    oid = (Oid)strtoul(oidstr, &end_ptr, 10);
    if (oid == 0 && (errno == ERANGE || errno == EINVAL)) {
        RETURN_FALSE;
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;
    PDO_DBH_CLEAR_ERR();

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    if (1 == lo_unlink(H->server, oid)) {
        RETURN_TRUE;
    }

    pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
    PDO_HANDLE_DBH_ERR();
    RETURN_FALSE;
}

/* {{{ Deletes the large object identified by oid.  Must be called inside a transaction. */
PHP_METHOD(PDO_PGSql_Ext, pgsqlLOBUnlink)
{
    swow_pgsqlLOBUnlink_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

void swow_pgsqlGetNotify_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;
    zend_long result_type = PDO_FETCH_USE_DEFAULT;
    zend_long ms_timeout = 0;
    PGnotify *pgsql_notify;

    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "|ll",
                &result_type, &ms_timeout)) {
        RETURN_THROWS();
    }

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;

    if (result_type == PDO_FETCH_USE_DEFAULT) {
        result_type = dbh->default_fetch_type;
    }

    if (result_type != PDO_FETCH_BOTH && result_type != PDO_FETCH_ASSOC && result_type != PDO_FETCH_NUM) {
        zend_argument_value_error(1, "must be one of PDO::FETCH_BOTH, PDO::FETCH_ASSOC, or PDO::FETCH_NUM");
        RETURN_THROWS();
    }

    if (ms_timeout < 0) {
        zend_argument_value_error(2, "must be greater than or equal to 0");
        RETURN_THROWS();
#ifdef ZEND_ENABLE_ZVAL_LONG64
    } else if (ms_timeout > INT_MAX) {
        php_error_docref(NULL, E_WARNING, "Timeout was shrunk to %d", INT_MAX);
        ms_timeout = INT_MAX;
#endif
    }

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    if (!PQconsumeInput(H->server)) {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
        PDO_HANDLE_DBH_ERR();
        RETURN_FALSE;
    }
    pgsql_notify = PQnotifies(H->server);

    if (ms_timeout && !pgsql_notify) {
        php_pollfd_for_ms(PQsocket(H->server), PHP_POLLREADABLE, (int)ms_timeout);

        if (!PQconsumeInput(H->server)) {
            pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, NULL);
            PDO_HANDLE_DBH_ERR();
            RETURN_FALSE;
        }
        pgsql_notify = PQnotifies(H->server);
    }

    if (!pgsql_notify) {
        RETURN_FALSE;
    }

    array_init(return_value);
    if (result_type == PDO_FETCH_NUM || result_type == PDO_FETCH_BOTH) {
        add_index_string(return_value, 0, pgsql_notify->relname);
        add_index_long(return_value, 1, pgsql_notify->be_pid);
        if (pgsql_notify->extra && pgsql_notify->extra[0]) {
            add_index_string(return_value, 2, pgsql_notify->extra);
        }
    }
    if (result_type == PDO_FETCH_ASSOC || result_type == PDO_FETCH_BOTH) {
        add_assoc_string(return_value, "message", pgsql_notify->relname);
        add_assoc_long(return_value, "pid", pgsql_notify->be_pid);
        if (pgsql_notify->extra && pgsql_notify->extra[0]) {
            add_assoc_string(return_value, "payload", pgsql_notify->extra);
        }
    }

    PQfreemem(pgsql_notify);
}

/* {{{ Get asynchronous notification */
PHP_METHOD(PDO_PGSql_Ext, pgsqlGetNotify)
{
    swow_pgsqlGetNotify_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

void swow_pgsqlGetPid_internal(INTERNAL_FUNCTION_PARAMETERS)
{
    pdo_dbh_t *dbh;
    pdo_pgsql_db_handle *H;

    ZEND_PARSE_PARAMETERS_NONE();

    dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK;

    H = (pdo_pgsql_db_handle *)dbh->driver_data;

    RETURN_LONG(PQbackendPID(H->server));
}

/* {{{ Get backend(server) pid */
PHP_METHOD(PDO_PGSql_Ext, pgsqlGetPid)
{
    swow_pgsqlGetPid_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

// diff since php/php-src@a9259c04969eefabf4c66a8843a66d0bee1c56c0
#if PHP_VERSION_ID > 80400
/* {{{ Sets a callback to receive DB notices (after client_min_messages has been set) */
PHP_METHOD(PDO_PGSql_Ext, pgsqlSetNoticeCallback)
{
    zend_fcall_info fci = empty_fcall_info;
    zend_fcall_info_cache fcc = empty_fcall_info_cache;
    if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "F!", &fci, &fcc)) {
        RETURN_THROWS();
    }

    pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
    PDO_CONSTRUCT_CHECK_WITH_CLEANUP(cleanup);

    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    swow_pdo_pgsql_cleanup_notice_callback(H);

    if (ZEND_FCC_INITIALIZED(fcc)) {
        H->notice_callback = emalloc(sizeof(zend_fcall_info_cache));
        zend_fcc_dup(H->notice_callback, &fcc);
    }

    return;

cleanup:
    if (ZEND_FCC_INITIALIZED(fcc)) {
        zend_fcc_dtor(&fcc);
    }
    RETURN_THROWS();
}
/* }}} */
#endif // PHP_VERSION_ID

static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind)
{
    switch (kind) {
        case PDO_DBH_DRIVER_METHOD_KIND_DBH:
            return class_PDO_PGSql_Ext_methods;
        default:
            return NULL;
    }
}

static swow_pdo_txn_bool pdo_pgsql_set_attr(pdo_dbh_t *dbh, zend_long attr, zval *val)
{
    bool bval;
    pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;

    switch (attr) {
        case PDO_ATTR_EMULATE_PREPARES:
            if (!pdo_get_bool_param(&bval, val)) {
                return false;
            }
            H->emulate_prepares = bval;
            return true;
        case PDO_PGSQL_ATTR_DISABLE_PREPARES:
            if (!pdo_get_bool_param(&bval, val)) {
                return false;
            }
            H->disable_prepares = bval;
            return true;
        case PDO_ATTR_PREFETCH:
            if (!pdo_get_bool_param(&bval, val)) {
                return false;
            }
            H->default_fetching_laziness = !bval;
            return true;
        default:
            return false;
    }
}

static const struct pdo_dbh_methods pgsql_methods = {
    pgsql_handle_closer,
    pgsql_handle_preparer,
    pgsql_handle_doer,
    pgsql_handle_quoter,
    pgsql_handle_begin,
    pgsql_handle_commit,
    pgsql_handle_rollback,
    pdo_pgsql_set_attr,
    pdo_pgsql_last_insert_id,
    pdo_pgsql_fetch_error_func,
    pdo_pgsql_get_attribute,
    pdo_pgsql_check_liveness,    /* check_liveness */
    pdo_pgsql_get_driver_methods,  /* get_driver_methods */
    NULL,
    pgsql_handle_in_transaction
// diff since php/php-src@e735de6eae4a60fb55fac6fc99b6b63f525c4b4b
#if PHP_VERSION_ID >= 80100
    , NULL /* get_gc */
#endif // PHP_VERSION_ID
// diff since php/php-src@715b9aaa09e1ad76a94f32b17da7927592fdae0a
#if PHP_VERSION_ID >= 80400
    , swow_pdo_pgsql_scanner /* scanner, not an exported symbol */
#endif // PHP_VERSION_ID
};

static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{ */
{
    pdo_pgsql_db_handle *H;
    int ret = 0;
    char *p, *e;
    zend_string *tmp_user, *tmp_pass;
    smart_str conn_str = {0};
    zend_long connect_timeout = 30;

    H = pecalloc(1, sizeof(pdo_pgsql_db_handle), dbh->is_persistent);
    dbh->driver_data = H;

    dbh->skip_param_evt =
        1 << PDO_PARAM_EVT_EXEC_POST |
        1 << PDO_PARAM_EVT_FETCH_PRE |
        1 << PDO_PARAM_EVT_FETCH_POST;

    H->einfo.errcode = 0;
    H->einfo.errmsg = NULL;

    /* PostgreSQL wants params in the connect string to be separated by spaces,
     * if the PDO standard semicolons are used, we convert them to spaces
     */
    e = (char *) dbh->data_source + dbh->data_source_len;
    p = (char *) dbh->data_source;
    while ((p = memchr(p, ';', (e - p)))) {
        *p = ' ';
    }

    if (driver_options) {
        connect_timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 30);
    }

    /* escape username and password, if provided */
    tmp_user = !strstr((char *) dbh->data_source, "user=") ? _pdo_pgsql_escape_credentials(dbh->username) : NULL;
    tmp_pass = !strstr((char *) dbh->data_source, "password=") ? _pdo_pgsql_escape_credentials(dbh->password) : NULL;

    smart_str_appendl(&conn_str, dbh->data_source, dbh->data_source_len);
    smart_str_append_printf(&conn_str, " connect_timeout=" ZEND_LONG_FMT, connect_timeout);

    /* support both full connection string & connection string + login and/or password */
    if (tmp_user) {
        smart_str_append_printf(&conn_str, " user='%s'", ZSTR_VAL(tmp_user));
    }

    if (tmp_pass) {
        smart_str_append_printf(&conn_str, " password='%s'", ZSTR_VAL(tmp_pass));
    }
    smart_str_0(&conn_str);

    H->server = cat_pq_connectdb(ZSTR_VAL(conn_str.s));
    H->lob_streams = (HashTable *) pemalloc(sizeof(HashTable), dbh->is_persistent);
    zend_hash_init(H->lob_streams, 0, NULL, NULL, 1);

    if (tmp_user) {
        zend_string_release_ex(tmp_user, 0);
    }
    if (tmp_pass) {
        zend_string_release_ex(tmp_pass, 0);
    }

    smart_str_free(&conn_str);

    if (PQstatus(H->server) != CONNECTION_OK) {
        pdo_pgsql_error(dbh, PGRES_FATAL_ERROR, PHP_PDO_PGSQL_CONNECTION_FAILURE_SQLSTATE);
        goto cleanup;
    }

    PQsetNoticeProcessor(H->server, (void(*)(void*,const char*))_pdo_pgsql_notice, (void *)dbh);

    H->attached = 1;
    H->pgoid = -1;

    dbh->methods = &pgsql_methods;
    dbh->alloc_own_columns = 1;
    dbh->max_escaped_char_length = 2;

    ret = 1;

cleanup:
    dbh->methods = &pgsql_methods;
    if (!ret) {
        pgsql_handle_closer(dbh);
    }

    return ret;
}
/* }}} */

const pdo_driver_t swow_pdo_pgsql_driver = {
    PDO_DRIVER_HEADER(pgsql),
    pdo_pgsql_handle_factory
};

#include "swow.h"

static PGresult *(*_swow_PQclosePrepared)(PGconn *conn, const char *stmtName) = NULL;
PGresult *swow_PQclosePrepared(PGconn *conn, const char *stmtName)
{
    if (_swow_PQclosePrepared) {
        return _swow_PQclosePrepared(conn, stmtName);
    }

    // TODO (??) libpq does not support close statement protocol < postgres 17
    // check if we can circumvent this.
    char *query;
    spprintf(&query, 0, "DEALLOCATE %s", stmtName);
    PGresult *res = cat_pq_exec(conn, query);
    efree(query);
    return res;
}

int swow_libpq_version = 0;
cat_bool_t swow_pgsql_hooked = cat_false;
size_t (*swow_PQresultMemorySize)(const PGresult *res) = NULL;

zend_result swow_pgsql_module_init(INIT_FUNC_ARGS)
{
    if (!SWOW_G(ini.hook_pdo_pgsql)) {
        return SUCCESS;
    }

    if (!zend_hash_str_exists(&module_registry, ZEND_STRL("pdo"))) {
        php_error_docref(NULL, E_WARNING, "Swow pdo_pgsql hook not enabled, pdo extension not enabled");
        return SUCCESS;
    }

#ifdef COMPILE_DL_SWOW
# if defined(CAT_OS_DARWIN)
    const char *library_paths[] = {
        // macports/brew
        "/opt/local/lib/",
        // brew
        "/usr/local/Cellar/libpq@17/lib/",
        "/usr/local/Cellar/libpq@16/lib/",
        "/usr/local/Cellar/libpq@15/lib/",
        "/usr/local/Cellar/libpq@14/lib/",
        // brew (new)
        // @see: https://earthly.dev/blog/homebrew-on-m1/)
        "/opt/homebrew/opt/libpq/lib/",
        // postgres.app
        "/Applications/Postgres.app/Contents/Versions/latest/lib/",
        // fink
        "/sw/lib/",
    };
    const char *library_names[] = {
        "libpq.5.dylib",
        "libpq.dylib",
    };
# elif defined(CAT_OS_WIN)
    const char *library_paths[] = {
        "C:\\Program Files\\PostgreSQL\\17\\bin\\",
        "C:\\Program Files\\PostgreSQL\\16\\bin\\",
        "C:\\Program Files\\PostgreSQL\\15\\bin\\",
        "C:\\Program Files\\PostgreSQL\\14\\bin\\",
        "C:\\Program Files (x86)\\PostgreSQL\\17\\bin\\",
        "C:\\Program Files (x86)\\PostgreSQL\\16\\bin\\",
        "C:\\Program Files (x86)\\PostgreSQL\\15\\bin\\",
        "C:\\Program Files (x86)\\PostgreSQL\\14\\bin\\",
    };
    const char *library_names[] = {
        "libpq.dll",
    };
# else
    const char *library_paths[] = { "" };
    const char *library_names[] = {
        "libpq." PHP_SHLIB_SUFFIX ".5",
        "libpq." PHP_SHLIB_SUFFIX,
    };
# endif

    DL_HANDLE dummy_handle = NULL;
    for (int i = 0; i < (int)CAT_ARRAY_SIZE(library_names); i++) {
        dummy_handle = DL_LOAD(library_names[i]);
        if (dummy_handle) {
            SWOW_G(libpq_so_name) = strdup(library_names[i]);
            break;
        }
    }
    if (!dummy_handle) {
        char name_buf[128];
        for (int i = 0; i < (int)CAT_ARRAY_SIZE(library_paths); i++) {
            for (int j = 0; j < (int)CAT_ARRAY_SIZE(library_names); j++) {
                snprintf(name_buf, sizeof(name_buf), "%s%s", library_paths[i], library_names[j]);

#ifdef CAT_OS_WIN
                SetDllDirectoryA(library_paths[i]);
#endif
                dummy_handle = DL_LOAD(name_buf);
#ifdef CAT_OS_WIN
                SetDllDirectoryA(NULL);
#endif
                if (dummy_handle) {
                    SWOW_G(libpq_so_name) = strdup(name_buf);
                    break;
                }
            }
        }
    }

    if (!dummy_handle) {
        smart_str paths = {0};
        smart_str names = {0};
        for (int i = 0; i < (int)CAT_ARRAY_SIZE(library_paths); i++) {
            smart_str_appends(&paths, library_paths[i]);
            smart_str_appendc(&paths, ',');
        }
        ZSTR_LEN(paths.s) -= 1;
        smart_str_0(&paths);

        for (int i = 0; i < (int)CAT_ARRAY_SIZE(library_names); i++) {
            smart_str_appends(&names, library_names[i]);
            smart_str_appends(&names, " or ");
        }
        ZSTR_LEN(names.s) -= 4;
        smart_str_0(&names);

        php_error_docref(NULL, E_WARNING, "Swow pdo_pgsql hook not enabled, %s not found, (search paths: %s)", ZSTR_VAL(names.s), ZSTR_VAL(paths.s));
        smart_str_free(&paths);
        smart_str_free(&names);
        return SUCCESS;
    }

    _swow_PQclosePrepared = (PGresult *(*)(PGconn *, const char *)) DL_FETCH_SYMBOL(dummy_handle, "PQclosePrepared");
    swow_PQresultMemorySize = (size_t (*)(const PGresult *)) DL_FETCH_SYMBOL(dummy_handle, "PQresultMemorySize");
#else
#if PG_VERSION_NUM >= 170000
    _swow_PQclosePrepared = PQclosePrepared;
#endif // PG_VERSION_NUM >= 170000
#if PG_VERSION_NUM >= 120000
    swow_PQresultMemorySize = PQresultMemorySize;
#endif // PG_VERSION_NUM >= 120000
#endif // COMPILE_DL_SWOW


    swow_libpq_version = PQlibVersion();
    if (swow_libpq_version < 90100) {
        php_error_docref(NULL, E_WARNING, "Swow pdo_pgsql hook not enabled, libpq is too old (%d < 90100)", swow_libpq_version);
        return SUCCESS;
    }

    // php_pdo_register_driver will unregister swow_pdo_pgsql_driver->driver_name i.e. "pdo_pgsql"
    php_pdo_unregister_driver(&swow_pdo_pgsql_driver);
    if (php_pdo_register_driver(&swow_pdo_pgsql_driver) == SUCCESS) {
        swow_pgsql_hooked = cat_true;
    }
    return SUCCESS;
}

zend_result swow_pgsql_module_shutdown(INIT_FUNC_ARGS)
{
    if (swow_pgsql_hooked) {
        php_pdo_unregister_driver(&swow_pdo_pgsql_driver);

        // we cannot get original driver, so whatever
        // if (swow_original_pdo_pgsql) {
        //        php_pdo_register_driver(swow_original_pdo_pgsql);
        // }

        swow_pgsql_hooked = cat_false;
        free((void *)SWOW_G(libpq_so_name));
        SWOW_G(libpq_so_name) = NULL;
    }

    return SUCCESS;
}

// compatibility

#if PHP_VERSION_ID < 80100
// from ext/pdo/pdo_dbh.c @ 715b9aaa09e1ad76a94f32b17da7927592fdae0a
bool pdo_get_long_param(zend_long *lval, const zval *value)
{
    switch (Z_TYPE_P(value)) {
        case IS_LONG:
        case IS_TRUE:
        case IS_FALSE:
            *lval = zval_get_long((zval *)value);
            return true;
        case IS_STRING:
            if (IS_LONG == is_numeric_str_function(Z_STR_P(value), lval, NULL)) {
                return true;
            }
            ZEND_FALLTHROUGH;
        default:
            zend_type_error("Attribute value must be of type int for selected attribute, %s given", zend_zval_value_name(value));
            return false;
    }
}

bool pdo_get_bool_param(bool *bval, const zval *value)
{
    switch (Z_TYPE_P(value)) {
        case IS_TRUE:
            *bval = true;
            return true;
        case IS_FALSE:
            *bval = false;
            return true;
        case IS_LONG:
            *bval = zend_is_true((zval *)value);
            return true;
        case IS_STRING: /* TODO Should string be allowed? */
        default:
            zend_type_error("Attribute value must be of type bool for selected attribute, %s given", zend_zval_value_name(value));
            return false;
    }
}
#endif // PHP_VERSION_ID < 80100

#if PHP_VERSION_ID < 80500
bool php_pdo_stmt_valid_db_obj_handle(const pdo_stmt_t *stmt)
{
    return !Z_ISUNDEF(stmt->database_object_handle)
        && IS_OBJ_VALID(EG(objects_store).object_buckets[Z_OBJ_HANDLE(stmt->database_object_handle)])
        && !(OBJ_FLAGS(Z_OBJ(stmt->database_object_handle)) & IS_OBJ_FREE_CALLED);
}
#endif // PHP_VERSION_ID < 80500

#endif /* CAT_PQ */
